[DRAFT] Метод POST /api/v1/webhooks/stores/receipt

Документация B2B API: Автоматическое зачисление продуктов через фискальный вебхук торговой сети Магазин

Published

July 1, 2026

WarningОграничение публичной документации

В открытом доступе представлена демонстрационная версия метода. В настоящей публичной документации отображены не все шаги, технические сценарии и приватные эндпоинты для системы цифровых симуляторов бизнес-процессов.

  • Полная спецификация метода: Будет доступна только во внутреннем контуре разработки (Confluence / Swagger Enterprise).

1 Функциональное назначение

Метод представляет собой открытый B2B-эндпоинт (вебхук), предназначенный для автоматического пополнения холодильника пользователя товарами, приобретенными в розничной сети или магазине или у продавца с настроенной интеграцией.

Метод решает следующие архитектурные задачи:

  1. Синхронный Direct-Inflow (Прямой импорт): В отличие от пользовательского контура (где фото чека уходит на OCR), “Вендор” передает серверу строго типизированный, очищенный JSON-датасет. Это позволяет системе зачислять продукты в холодильник обходя буферные таблицы pending_receipts.
  2. Сквозная B2B-авторизация: Метод защищен протоколом OAuth2 Client Credentials или выделенным статическим API-ключом, так как запрос инициируется сервером торговой сети, а не мобильным приложением.
  3. Связывание аккаунтов (User Mapping): Идентификация целевого холодильника (home_group_id) происходит по переданному в теле запроса loyalty_card_token (токен карты лояльности Магазин, которую пользователь привязал в своем профиле FoodLifeCycleApp).

2 Протокол взаимодействия (HTTP Контракт)

  • Метод: POST
  • Маршрут: /api/v1/webhooks/stores/receipt
  • Формат данных: application/json

2.1 Спецификация заголовков (HTTP Headers)

Заголовок Обязательный Описание Пример значения
Content-Type Да Указывает на передачу строго типизированного JSON-пакета application/json
X-Shop-Token Да Секретный статический ключ авторизации партнера (B2B API Key) shop_live_secret_abc123...
X-Request-ID Да Сквозной ID запроса, генерируемый шиной Magnum для трассировки shop-tr-44bb-99ff

2.2 Спецификация тела запроса (Request Body)

Структура данных полностью повторяет фискальный срез чека Магазин. Поля количества товара (quantity) поддерживают тип float/double для корректного зачисления весовых позиций (например, бананов весом 1.695 кг).

Поле Тип Обязательный Описание Пример значения
loyalty_card_token String Да Уникальный токен карты, по которому бэкенд находит user_id и home_group_id shop-card-8822-fa
receipt_number String Да Фискальный номер чека для предотвращения дублирования CH-20260701-09
items Array Да Список приобретенных товарных позиций [..._name: "БАНАН", qty: 1.695]

2.2.1 Пример сырого JSON-запроса (Payload):

{
  "loyalty_card_token": "shop-card-8822-fa",
  "receipt_number": "CH-20260701-09",
  "purchase_timestamp": "2026-07-01T23:10:00Z",
  "store_id": "shop-almaty-05",
  "items": [
    {
      "product_id": "2110310",
      "product_name": "БАНАН ЭКВАДОР КГ",
      "quantity": 1.6950,
      "price": 595.00,
      "unit": "кг"
    },
    {
      "product_id": "4001020",
      "product_name": "ХЛЕБ АКСАЙ БОРОДИНСКИЙ",
      "quantity": 1.0000,
      "price": 180.00,
      "unit": "шт"
    }
  ]
}

3 Схема обработки запроса пользователя (Диаграмма последователности на Mermaid)

На диаграмме представлена логика сквозной интеграции через B2B-вебхук: бэкенд идентифицирует пользователя по карте лояльности, проверяет чек на дубликаты и зачисляет весовую матрицу товаров напрямую в инвентарь PostgreSQL без ручного вмешательства.

sequenceDiagram
    autonumber
    actor Partner as REST-клиент Торговой Сети
    participant Nginx as Nginx Proxy
    participant GW as FastAPI Gateway (backend-api)
    participant Store as store-service (INVENTORY)
    participant Auth as auth-service (SECURITY/IAD)
    participant K as Apache Kafka Cluster
    participant Fridge as fridge-service (INVENTORY)

    %% ПОТОК ОБРАБОТКИ ВЕБХУКА И gRPC ВАЛИДАЦИЯ
    Partner->>Nginx: POST /api/v1/webhooks/stores/receipt (Authorization: shop_token)
    Nginx->>GW: Внутренний прокси сырого JSON вебхука
    GW->>Store: Запрос к сервису Store
    Store->>Auth: gRPC: validateStoreToken(shop_token, shop_id)
    
    alt Сценарий 3а: Токен НЕ валиден / Магазин заблокирован
        Auth-->>Store: gRPC Response: INVALID / BLOCKED
        Store-->>Partner: HTTP 401 Unauthorized
    else Сценарий 3б: Токен валиден (Успех)
        Auth-->>Store: gRPC Response: VALID (OK)
        Store-->>Partner: HTTP 200 OK (Контракт исполнен, Fire-and-Forget)
        
        %% АСИНХРОННЫЙ ХВОСТ ПРЯМОГО ЗАЧИСЛЕНИЯ
        Store->>K: Пуш в топик: bpds.inventory.in.fridge_item.deduct
        K->>Fridge: Handler: ProcessTrustedB2BReceipt()
        Note over Fridge: Шаг 7: Прямой транзакционный INSERT<br/>в PostgreSQL Fridge DB на баланс юзера
    end

4 Расшифровка шагов

Шаг Действие Параметры / Запросы Ошибки (Исключения / Статусы)
Шаг 1 (Partner -> Nginx) Внешний REST-клиент торговой сети отправляет сырой JSON вебхука с данными чека. HTTP POST /api/v1/webhooks/stores/receipt
Headers: Authorization: shop_token
DioException: send timeout
HTTP 400 Bad Request (сломанный JSON)
Шаг 2 (Nginx -> GW) Шлюз выполняет внутреннее проксирование входящего тела запроса в шлюз. Внутренний проброс сырого JSON вебхука HTTP 502 Bad Gateway
HTTP 504 Gateway Timeout
Шаг 3 (GW -> Store) Шлюз выполняет внутренний проброс входящего тела запроса на служебный сервис. Внутренний проброс сырого JSON вебхука HTTP 502 Bad Gateway
HTTP 504 Gateway Timeout
Шаг 4 (Store -> Auth) Сервис инвентаря запрашивает верификацию токена магазина у компонента безопасности. Метод: gRPC validateStoreToken(shop_token, shop_id) Таймаут gRPC: gRPC: DeadlineExceeded
HTTP 503 Service Unavailable
Шаг 5 (Auth -> Store) Сценарий 3а: Компонент безопасности возвращает статус невалидного или заблокированного токена. gRPC Response: INVALID / BLOCKED gRPC: Internal Error
Шаг 6 (Store -> Partner) Сценарий 3а: Сервис инвентаря отклоняет запрос торговой сети из-за провала авторизации. HTTP 401 Unauthorized
Body: Ошибка авторизации токена партнера
HTTP 500 Serialization Error
Шаг 7 (Auth -> Store) Сценарий 3б: Компонент безопасности подтверждает валидность и активный статус партнера. gRPC Response: VALID (OK) gRPC: Internal Error
Шаг 8 (Store -> Partner) Сценарий 3б: Сервис завершает обработку синхронного периметра и отпускает клиента. HTTP 200 OK
Парадигма: Fire-and-Forget (Контракт исполнен)
HTTP 500 Serialization Error
Шаг 9 (Store -> KAFKA) Сценарий 3б: Сервис асинхронно публикует задачу списания/начисления продуктов в шину данных. Топик: bpds.inventory.in.fridge_item.deduct
Параметры: max.block.ms = 1000
Сбой шины: Kafka: TimeoutException
Сброс логов в Promtail для ручного восстановления
Шаг 10 (KAFKA -> Fridge) Сценарий 3б: Сервер инвентаря холодильника вычитывает доверенное B2B-сообщение из топика. Топик: bpds.inventory.in.fridge_item.deduct
Handler: ProcessTrustedB2BReceipt()
Kafka: CommitFailedException
Шаг 11 (Fridge -> СУБД) Сценарий 3б (Внутреннее действие): Для защиты от повторных зачислений продуктов при сбоях сети на стороне партнера (Idempotency / Идемпотентность), бэкенд выполняет проверку SQL-запрос:
SELECT 1 FROM processed_b2b_receipts WHERE partner_code = 'MAGAZIN' AND receipt_number = $1.
PostgreSQL Exception: ConnectionException (отказ пула соединений БД)
Шаг 12 (СУБД -> Fridge) База данных возвращает пустой ответ, подтверждая, что данный фискальный документ уникален и обрабатывается нашей системой впервые. Вилка исключений (Дубликат): Если строка найдена, бэкенд прерывает выполнение и возвращает статус предотвращая дублирование продуктов в холодильнике. HTTP 409 Conflict,
Шаг 13 (Fridge -> СУБД)
Контекст HOME/OFFICE
Сервис холодильника выполняет SQL-запрос последовательно выполняет DML-команды вставки и обновляет баланс продуктов на аккаунте конечного пользователя. SQL-запрос:
INSERT INTO fridge_inventory (user_id, home_group_id, product_name, quantity, unit) VALUES (\$1, \$2, \$3, \$4, \$5);
PostgreSQL Exception: DeadlockDetected
PostgreSQL Exception: ConnectionException (отказ пула соединений БД)
Шаг 14 (СУБД -> Fridge) TBD TBD TBD
Шаг 15 (Fridge -> Kafka) Бэкенд асинхронно отправляет системное бизнес-событие в топик Kafka TBD для уведомления смежных микросервисов (аналитика, умные рецепты) о пополнении запасов. TBD TBD
Шаг 16 (Fridge -> Store) Шлюз возвращает серверу Magnum успешный ответ HTTP 200 OK с подтверждением приема данных и количеством зачисленных позиций. TBD TBD
Шаг 17 (Fridge -> App) Чтобы пользователю не приходилось вручную обновлять экран смартфона, FastAPI находит активное WebSocket-соединение семьи по home_group_id и отправляет сигнал "STOCK_INTEGRATION_UPDATED". На стороне APP вкладка холодильника мгновенно перерисовывается, отображая свежие бананы и хлеб, купленные в магазине минуту назад. TBD TBD

5 Спецификация ответов сервера (Response Body) и ошибок

5.1 Успешный ответ (Success Response)

5.1.1 HTTP 200 OK (Ответ на Шаге 11)

Возвращается шине данных Magnum в качестве успешного подтверждения приема, дедупликации и зачисления всего массива товаров из чека в базу данных.

  • Заголовки ответа (Response Headers):
    • Content-Type: application/json
  • Тело ответа (Response Body):
{
  "status": "PROCESSED",
  "receipt_number": "CH-20260701-09",
  "loyalty_card_token": "shop-card-8822-fa",
  "metrics": {
    "items_processed_count": 2,
    "db_transaction_status": "COMMITTED"
  },
  "timestamp": "2026-07-01T23:10:02Z"
}

5.2 Спецификация ошибок и вилок исключений (Error Responses)

5.2.1 Ошибка дублирования чека (HTTP 409 Conflict — Шаг 6)

Выбрасывается на Шаге 5, если переданный receipt_number уже присутствует в таблице дедупликации processed_b2b_receipts. Это предотвращает повторное начисление веса продуктов при сбоях и ретраях на стороне Magnum.

{
  "error_code": "ERR-RECEIPT-DUPLICATE",
  "message": "Данный фискальный документ уже был успешно обработан и зачислен ранее.",
  "details": {
    "rejected_receipt_number": "CH-20260701-09",
    "partner_code": "MAGNUM",
    "action": "Idempotency trigger activated. No duplicate database writes executed."
  }
}

5.2.2 Ошибка: Карта лояльности не привязана к профилю (HTTP 422 Unprocessable Entity — Шаг 4)

Выбрасывается на Шаге 4, если loyalty_card_token верный по структуре, но отсутствует в таблице маппинга user_loyalty_cards. Система не может определить, в чей именно холодильник нужно положить продукты.

{
  "error_code": "ERR-LOYALTY-CARD-NOT-MAPPED",
  "message": "Карта лояльности партнера не связана ни с одним активным аккаунтом в экосистеме .",
  "details": {
    "unmapped_token": "mag-card-8822-fa",
    "action": "The user must physically link their Magnum card inside the Flutter mobile app profile."
  }
}

5.2.3 Ошибка B2B-авторизации партнера (HTTP 401 Unauthorized — Шаг 2)

Выбрасывается на Шаге 2 бэкенд-шлюзом FastAPI, если заголовок X-Shop-Token отсутствует, заблокирован или содержит неверный API-ключ партнера.

{
  "error_code": "ERR-B2B-UNAUTHORIZED",
  "message": "B2B API токен партнера не прошел валидацию. Доступ к вебхуку отклонен.",
  "details": {
    "reason": "Invalid or revoked X-Shop-Token credential sequence."
  }
}